11 require_once( 'WatchedItem.php' );
13 # Number of characters in user_token field
14 define( 'USER_TOKEN_LENGTH', 32 );
16 # Serialized record version
17 define( 'MW_USER_VERSION', 2 );
27 var $mId, $mName, $mPassword, $mEmail, $mNewtalk;
28 var $mEmailAuthenticated;
29 var $mRights, $mOptions;
30 var $mDataLoaded, $mNewpassword;
32 var $mBlockedby, $mBlockreason;
38 var $mVersion; // serialized version
40 /** Construct using User:loadDefaults() */
42 $this->loadDefaults();
43 $this->mVersion
= MW_USER_VERSION
;
47 * Static factory method
48 * @param string $name Username, validated by Title:newFromText()
52 function newFromName( $name ) {
55 # Force usernames to capital
57 $name = $wgContLang->ucfirst( $name );
59 # Clean up name according to title rules
60 $t = Title
::newFromText( $name );
65 # Reject various classes of invalid names
66 $canonicalName = $t->getText();
67 if( !User
::isValidUserName( $canonicalName ) ) {
71 $u->setName( $canonicalName );
72 $u->setId( $u->idFromName( $t->getText() ) );
77 * Factory method to fetch whichever use has a given email confirmation code.
78 * This code is generated when an account is created or its e-mail address
81 * If the code is invalid or has expired, returns NULL.
87 function newFromConfirmationCode( $code ) {
88 $dbr =& wfGetDB( DB_SLAVE
);
89 $name = $dbr->selectField( 'user', 'user_name', array(
90 'user_email_token' => md5( $code ),
91 'user_email_token_expires > ' . $dbr->addQuotes( $dbr->timestamp() ),
93 if( is_string( $name ) ) {
94 return User
::newFromName( $name );
101 * Serialze sleep function, for better cache efficiency and avoidance of
102 * silly "incomplete type" errors when skins are cached
105 return array( 'mId', 'mName', 'mPassword', 'mEmail', 'mNewtalk',
106 'mEmailAuthenticated', 'mRights', 'mOptions', 'mDataLoaded',
107 'mNewpassword', 'mBlockedby', 'mBlockreason', 'mTouched',
108 'mToken', 'mRealName', 'mHash', 'mGroups' );
112 * Get username given an id.
113 * @param integer $id Database user id
114 * @return string Nickname of a user
117 function whoIs( $id ) {
118 $dbr =& wfGetDB( DB_SLAVE
);
119 return $dbr->selectField( 'user', 'user_name', array( 'user_id' => $id ) );
123 * Get real username given an id.
124 * @param integer $id Database user id
125 * @return string Realname of a user
128 function whoIsReal( $id ) {
129 $dbr =& wfGetDB( DB_SLAVE
);
130 return $dbr->selectField( 'user', 'user_real_name', array( 'user_id' => $id ) );
134 * Get database id given a user name
135 * @param string $name Nickname of a user
136 * @return integer|null Database user id (null: if non existent
139 function idFromName( $name ) {
140 $fname = "User::idFromName";
142 $nt = Title
::newFromText( $name );
143 if( is_null( $nt ) ) {
147 $dbr =& wfGetDB( DB_SLAVE
);
148 $s = $dbr->selectRow( 'user', array( 'user_id' ), array( 'user_name' => $nt->getText() ), $fname );
150 if ( $s === false ) {
158 * does the string match an anonymous IPv4 address?
161 * @param string $name Nickname of a user
164 function isIP( $name ) {
165 return preg_match("/^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/",$name);
166 /*return preg_match("/^
167 (?:[01]?\d{1,2}|2(:?[0-4]\d|5[0-5]))\.
168 (?:[01]?\d{1,2}|2(:?[0-4]\d|5[0-5]))\.
169 (?:[01]?\d{1,2}|2(:?[0-4]\d|5[0-5]))\.
170 (?:[01]?\d{1,2}|2(:?[0-4]\d|5[0-5]))
175 * Is the input a valid username?
177 * Checks if the input is a valid username, we don't want an empty string,
178 * an IP address, anything that containins slashes (would mess up subpages),
179 * is longer than the maximum allowed username size or doesn't begin with
182 * @param string $name
186 function isValidUserName( $name ) {
187 global $wgContLang, $wgMaxNameChars;
190 || User
::isIP( $name )
191 ||
strpos( $name, '/' ) !== false
192 ||
strlen( $name ) > $wgMaxNameChars
193 ||
$name != $wgContLang->ucfirst( $name ) )
200 * Is the input a valid password?
202 * @param string $password
206 function isValidPassword( $password ) {
207 global $wgMinimalPasswordLength;
208 return strlen( $password ) >= $wgMinimalPasswordLength;
212 * does the string match roughly an email address ?
214 * @todo Check for RFC 2822 compilance
217 * @param string $addr email address
221 function isValidEmailAddr ( $addr ) {
222 # There used to be a regular expression here, it got removed because it
223 # rejected valid addresses.
224 return ( trim( $addr ) != '' ) &&
225 (false !== strpos( $addr, '@' ) );
229 * Count the number of edits of a user
231 * @param int $uid The user ID to check
234 function edits( $uid ) {
235 $fname = 'User::editCount';
237 $dbr =& wfGetDB( DB_SLAVE
);
238 return $dbr->selectField(
239 'revision', 'count(*)',
240 array( 'rev_user' => $uid ),
246 * Count the number of edits of a user by namespace
248 * @param int $uid The user ID to check
251 function editsByNs( $uid ) {
252 $fname = 'User::editsByNs';
255 $dbr =& wfGetDB( DB_SLAVE
);
257 array( 'user', 'revision', 'page' ),
258 array( 'page_namespace', 'COUNT(*) as count' ),
261 'rev_user' => array( false, 'user_id' ),
262 'rev_page' => array( false, 'page_id' )
265 array( 'GROUP BY' => 'page_namespace' )
268 while( $row = $dbr->fetchObject( $res ) ) {
269 $nscount[$row->page_namespace
] = $row->count
;
276 * probably return a random password
277 * @return string probably a random password
279 * @todo Check what is doing really [AV]
281 function randomPassword() {
282 $pwchars = 'ABCDEFGHJKLMNPQRSTUVWXYZabcdefghjkmnpqrstuvwxyz';
283 $l = strlen( $pwchars ) - 1;
285 $np = $pwchars{mt_rand( 0, $l )} . $pwchars{mt_rand( 0, $l )} .
286 $pwchars{mt_rand( 0, $l )} . chr( mt_rand(48, 57) ) .
287 $pwchars{mt_rand( 0, $l )} . $pwchars{mt_rand( 0, $l )} .
288 $pwchars{mt_rand( 0, $l )};
293 * Set properties to default
294 * Used at construction. It will load per language default settings only
295 * if we have an available language object.
297 function loadDefaults() {
300 $fname = 'User::loadDefaults' . $n;
301 wfProfileIn( $fname );
303 global $wgContLang, $wgIP, $wgDBname;
304 global $wgNamespacesToBeSearchedDefault;
307 $this->mNewtalk
= -1;
308 $this->mName
= $wgIP;
309 $this->mRealName
= $this->mEmail
= '';
310 $this->mEmailAuthenticated
= null;
311 $this->mPassword
= $this->mNewpassword
= '';
312 $this->mRights
= array();
313 $this->mGroups
= array();
314 $this->mOptions
= User
::getDefaultOptions();
316 foreach( $wgNamespacesToBeSearchedDefault as $nsnum => $val ) {
317 $this->mOptions
['searchNs'.$nsnum] = $val;
319 unset( $this->mSkin
);
320 $this->mDataLoaded
= false;
321 $this->mBlockedby
= -1; # Unset
322 $this->setToken(); # Random
323 $this->mHash
= false;
325 if ( isset( $_COOKIE[$wgDBname.'LoggedOut'] ) ) {
326 $this->mTouched
= wfTimestamp( TS_MW
, $_COOKIE[$wgDBname.'LoggedOut'] );
329 $this->mTouched
= '0'; # Allow any pages to be cached
332 wfProfileOut( $fname );
336 * Combine the language default options with any site-specific options
337 * and add the default language variants.
343 function getDefaultOptions() {
345 * Site defaults will override the global/language defaults
347 global $wgContLang, $wgDefaultUserOptions;
348 $defOpt = $wgDefaultUserOptions +
$wgContLang->getDefaultUserOptions();
351 * default language setting
353 $variant = $wgContLang->getPreferredVariant();
354 $defOpt['variant'] = $variant;
355 $defOpt['language'] = $variant;
361 * Get a given default option value.
368 function getDefaultOption( $opt ) {
369 $defOpts = User
::getDefaultOptions();
370 if( isset( $defOpts[$opt] ) ) {
371 return $defOpts[$opt];
378 * Get blocking information
380 * @param bool $bFromSlave Specify whether to check slave or master. To improve performance,
381 * non-critical checks are done against slaves. Check when actually saving should be done against
384 * Note that even if $bFromSlave is false, the check is done first against slave, then master.
385 * The logic is that if blocked on slave, we'll assume it's either blocked on master or
386 * just slightly outta sync and soon corrected - safer to block slightly more that less.
387 * And it's cheaper to check slave first, then master if needed, than master always.
389 function getBlockedStatus( $bFromSlave = true ) {
390 global $wgIP, $wgBlockCache, $wgProxyList, $wgEnableSorbs, $wgProxyWhitelist;
392 if ( -1 != $this->mBlockedby
) { return; }
394 $this->mBlockedby
= 0;
398 $block = new Block();
399 $block->forUpdate( $bFromSlave );
400 if ( $block->load( $wgIP , $this->mId
) ) {
401 $this->mBlockedby
= $block->mBy
;
402 $this->mBlockreason
= $block->mReason
;
403 $this->spreadBlock();
408 if ( !$this->mBlockedby
) {
409 # Check first against slave, and optionally from master.
410 $block = $wgBlockCache->get( $wgIP, true );
411 if ( !$block && !$bFromSlave )
413 # Not blocked: check against master, to make sure.
414 $wgBlockCache->clearLocal( );
415 $block = $wgBlockCache->get( $wgIP, false );
417 if ( $block !== false ) {
418 $this->mBlockedby
= $block->mBy
;
419 $this->mBlockreason
= $block->mReason
;
424 if ( !$this->isSysop() && !in_array( $wgIP, $wgProxyWhitelist ) ) {
427 if ( array_key_exists( $wgIP, $wgProxyList ) ) {
428 $this->mBlockedby
= wfMsg( 'proxyblocker' );
429 $this->mBlockreason
= wfMsg( 'proxyblockreason' );
433 if ( !$this->mBlockedby
&& $wgEnableSorbs && !$this->getID() ) {
434 if ( $this->inSorbsBlacklist( $wgIP ) ) {
435 $this->mBlockedby
= wfMsg( 'sorbs' );
436 $this->mBlockreason
= wfMsg( 'sorbsreason' );
442 function inSorbsBlacklist( $ip ) {
443 global $wgEnableSorbs;
444 return $wgEnableSorbs &&
445 $this->inDnsBlacklist( $ip, 'http.dnsbl.sorbs.net.' );
448 function inOpmBlacklist( $ip ) {
450 return $wgEnableOpm &&
451 $this->inDnsBlacklist( $ip, 'opm.blitzed.org.' );
454 function inDnsBlacklist( $ip, $base ) {
455 $fname = 'User::inDnsBlacklist';
456 wfProfileIn( $fname );
461 if ( preg_match( '/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/', $ip, $m ) ) {
463 for ( $i=4; $i>=1; $i-- ) {
464 $host .= $m[$i] . '.';
469 $ipList = gethostbynamel( $host );
472 wfDebug( "Hostname $host is {$ipList[0]}, it's a proxy says $base!\n" );
475 wfDebug( "Requested $host, not found in $base.\n" );
479 wfProfileOut( $fname );
484 * Primitive rate limits: enforce maximum actions per time period
485 * to put a brake on flooding.
487 * Note: when using a shared cache like memcached, IP-address
488 * last-hit counters will be shared across wikis.
490 * @return bool true if a rate limiter was tripped
493 function pingLimiter( $action='edit' ) {
494 global $wgRateLimits;
495 if( !isset( $wgRateLimits[$action] ) ) {
498 if( $this->isAllowed( 'delete' ) ) {
503 global $wgMemc, $wgIP, $wgDBname, $wgRateLimitLog;
504 $fname = 'User::pingLimiter';
505 $limits = $wgRateLimits[$action];
507 $id = $this->getId();
509 if( isset( $limits['anon'] ) && $id == 0 ) {
510 $keys["$wgDBname:limiter:$action:anon"] = $limits['anon'];
513 if( isset( $limits['user'] ) && $id != 0 ) {
514 $keys["$wgDBname:limiter:$action:user:$id"] = $limits['user'];
516 if( $this->isNewbie() ) {
517 if( isset( $limits['newbie'] ) && $id != 0 ) {
518 $keys["$wgDBname:limiter:$action:user:$id"] = $limits['newbie'];
520 if( isset( $limits['ip'] ) ) {
521 $keys["mediawiki:limiter:$action:ip:$wgIP"] = $limits['ip'];
523 if( isset( $limits['subnet'] ) && preg_match( '/^(\d+\.\d+\.\d+)\.\d+$/', $wgIP, $matches ) ) {
524 $subnet = $matches[1];
525 $keys["mediawiki:limiter:$action:subnet:$subnet"] = $limits['subnet'];
530 foreach( $keys as $key => $limit ) {
531 list( $max, $period ) = $limit;
532 $summary = "(limit $max in {$period}s)";
533 $count = $wgMemc->get( $key );
535 if( $count > $max ) {
536 wfDebug( "$fname: tripped! $key at $count $summary\n" );
537 if( $wgRateLimitLog ) {
538 @error_log
( wfTimestamp( TS_MW
) . ' ' . $wgDBname . ': ' . $this->getName() . " tripped $key at $count $summary\n", 3, $wgRateLimitLog );
542 wfDebug( "$fname: ok. $key at $count $summary\n" );
545 wfDebug( "$fname: adding record for $key $summary\n" );
546 $wgMemc->add( $key, 1, IntVal( $period ) );
548 $wgMemc->incr( $key );
555 * Check if user is blocked
556 * @return bool True if blocked, false otherwise
558 function isBlocked( $bFromSlave = false ) {
559 $this->getBlockedStatus( $bFromSlave );
560 return $this->mBlockedby
!== 0;
564 * Check if user is blocked from editing a particular article
566 function isBlockedFrom( $title, $bFromSlave = false ) {
567 global $wgBlockAllowsUTEdit;
568 if ( $wgBlockAllowsUTEdit && $title->getText() === $this->getName() &&
569 $title->getNamespace() == NS_USER_TALK
)
573 return $this->isBlocked( $bFromSlave );
578 * Get name of blocker
579 * @return string name of blocker
581 function blockedBy() {
582 $this->getBlockedStatus();
583 return $this->mBlockedby
;
587 * Get blocking reason
588 * @return string Blocking reason
590 function blockedFor() {
591 $this->getBlockedStatus();
592 return $this->mBlockreason
;
596 * Initialise php session
598 function SetupSession() {
599 global $wgSessionsInMemcached, $wgCookiePath, $wgCookieDomain;
600 if( $wgSessionsInMemcached ) {
601 require_once( 'MemcachedSessions.php' );
602 } elseif( 'files' != ini_get( 'session.save_handler' ) ) {
603 # If it's left on 'user' or another setting from another
604 # application, it will end up failing. Try to recover.
605 ini_set ( 'session.save_handler', 'files' );
607 session_set_cookie_params( 0, $wgCookiePath, $wgCookieDomain );
608 session_cache_limiter( 'private, must-revalidate' );
613 * Read datas from session
616 function loadFromSession() {
617 global $wgMemc, $wgDBname;
619 if ( isset( $_SESSION['wsUserID'] ) ) {
620 if ( 0 != $_SESSION['wsUserID'] ) {
621 $sId = $_SESSION['wsUserID'];
625 } else if ( isset( $_COOKIE["{$wgDBname}UserID"] ) ) {
626 $sId = IntVal( $_COOKIE["{$wgDBname}UserID"] );
627 $_SESSION['wsUserID'] = $sId;
631 if ( isset( $_SESSION['wsUserName'] ) ) {
632 $sName = $_SESSION['wsUserName'];
633 } else if ( isset( $_COOKIE["{$wgDBname}UserName"] ) ) {
634 $sName = $_COOKIE["{$wgDBname}UserName"];
635 $_SESSION['wsUserName'] = $sName;
640 $passwordCorrect = FALSE;
641 $user = $wgMemc->get( $key = "$wgDBname:user:id:$sId" );
642 if( !is_object( $user ) ||
$user->mVersion
< MW_USER_VERSION
) {
643 # Expire old serialized objects; they may be corrupt.
646 if($makenew = !$user) {
647 wfDebug( "User::loadFromSession() unable to load from memcached\n" );
650 $user->loadFromDatabase();
652 wfDebug( "User::loadFromSession() got from cache!\n" );
655 if ( isset( $_SESSION['wsToken'] ) ) {
656 $passwordCorrect = $_SESSION['wsToken'] == $user->mToken
;
657 } else if ( isset( $_COOKIE["{$wgDBname}Token"] ) ) {
658 $passwordCorrect = $user->mToken
== $_COOKIE["{$wgDBname}Token"];
660 return new User(); # Can't log in from session
663 if ( ( $sName == $user->mName
) && $passwordCorrect ) {
665 if($wgMemc->set( $key, $user ))
666 wfDebug( "User::loadFromSession() successfully saved user\n" );
668 wfDebug( "User::loadFromSession() unable to save to memcached\n" );
672 return new User(); # Can't log in from session
676 * Load a user from the database
678 function loadFromDatabase() {
679 global $wgCommandLineMode;
680 $fname = "User::loadFromDatabase";
682 # Counter-intuitive, breaks various things, use User::setLoaded() if you want to suppress
683 # loading in a command line script, don't assume all command line scripts need it like this
684 #if ( $this->mDataLoaded || $wgCommandLineMode ) {
685 if ( $this->mDataLoaded
) {
690 $this->mId
= IntVal( $this->mId
);
692 /** Anonymous user */
695 $this->mRights
= $this->getGroupPermissions( array( '*' ) );
696 $this->mDataLoaded
= true;
698 } # the following stuff is for non-anonymous users only
700 $dbr =& wfGetDB( DB_SLAVE
);
701 $s = $dbr->selectRow( 'user', array( 'user_name','user_password','user_newpassword','user_email',
702 'user_email_authenticated',
703 'user_real_name','user_options','user_touched', 'user_token' ),
704 array( 'user_id' => $this->mId
), $fname );
706 if ( $s !== false ) {
707 $this->mName
= $s->user_name
;
708 $this->mEmail
= $s->user_email
;
709 $this->mEmailAuthenticated
= wfTimestampOrNull( TS_MW
, $s->user_email_authenticated
);
710 $this->mRealName
= $s->user_real_name
;
711 $this->mPassword
= $s->user_password
;
712 $this->mNewpassword
= $s->user_newpassword
;
713 $this->decodeOptions( $s->user_options
);
714 $this->mTouched
= wfTimestamp(TS_MW
,$s->user_touched
);
715 $this->mToken
= $s->user_token
;
717 $res = $dbr->select( 'user_groups',
719 array( 'ug_user' => $this->mId
),
721 $this->mGroups
= array();
722 while( $row = $dbr->fetchObject( $res ) ) {
723 $this->mGroups
[] = $row->ug_group
;
725 $effectiveGroups = array_merge( array( '*', 'user' ), $this->mGroups
);
726 $this->mRights
= $this->getGroupPermissions( $effectiveGroups );
729 $this->mDataLoaded
= true;
732 function getID() { return $this->mId
; }
733 function setID( $v ) {
735 $this->mDataLoaded
= false;
739 $this->loadFromDatabase();
743 function setName( $str ) {
744 $this->loadFromDatabase();
750 * Return the title dbkey form of the name, for eg user pages.
754 function getTitleKey() {
755 return str_replace( ' ', '_', $this->getName() );
758 function getNewtalk() {
760 $fname = 'User::getNewtalk';
761 $this->loadFromDatabase();
763 # Load the newtalk status if it is unloaded (mNewtalk=-1)
764 if( $this->mNewtalk
== -1 ) {
765 $this->mNewtalk
= 0; # reset talk page status
767 # Check memcached separately for anons, who have no
768 # entire User object stored in there.
770 global $wgDBname, $wgMemc;
771 $key = "$wgDBname:newtalk:ip:{$this->mName}";
772 $newtalk = $wgMemc->get( $key );
773 if( is_integer( $newtalk ) ) {
774 $this->mNewtalk
= $newtalk ?
1 : 0;
775 return (bool)$this->mNewtalk
;
779 $dbr =& wfGetDB( DB_SLAVE
);
780 if ( $wgUseEnotif ) {
781 $res = $dbr->select( 'watchlist',
783 array( 'wl_title' => $this->getTitleKey(),
784 'wl_namespace' => NS_USER_TALK
,
785 'wl_user' => $this->mId
,
786 'wl_notificationtimestamp != 0' ),
787 'User::getNewtalk' );
788 if( $dbr->numRows($res) > 0 ) {
791 $dbr->freeResult( $res );
792 } elseif ( $this->mId
) {
793 $res = $dbr->select( 'user_newtalk', 1, array( 'user_id' => $this->mId
), $fname );
795 if ( $dbr->numRows($res)>0 ) {
798 $dbr->freeResult( $res );
800 $res = $dbr->select( 'user_newtalk', 1, array( 'user_ip' => $this->mName
), $fname );
801 $this->mNewtalk
= $dbr->numRows( $res ) > 0 ?
1 : 0;
802 $dbr->freeResult( $res );
806 $wgMemc->set( $key, $this->mNewtalk
, time() ); // + 1800 );
810 return ( 0 != $this->mNewtalk
);
813 function setNewtalk( $val ) {
814 $this->loadFromDatabase();
815 $this->mNewtalk
= $val;
816 $this->invalidateCache();
819 function invalidateCache() {
820 global $wgClockSkewFudge;
821 $this->loadFromDatabase();
822 $this->mTouched
= wfTimestamp(TS_MW
, time() +
$wgClockSkewFudge );
823 # Don't forget to save the options after this or
824 # it won't take effect!
827 function validateCache( $timestamp ) {
828 $this->loadFromDatabase();
829 return ($timestamp >= $this->mTouched
);
834 * Will only be salted if $wgPasswordSalt is true
835 * @param string Password.
836 * @return string Salted password or clear password.
838 function addSalt( $p ) {
839 global $wgPasswordSalt;
841 return md5( "{$this->mId}-{$p}" );
847 * Encrypt a password.
848 * It can eventuall salt a password @see User::addSalt()
849 * @param string $p clear Password.
850 * @param string Encrypted password.
852 function encryptPassword( $p ) {
853 return $this->addSalt( md5( $p ) );
856 # Set the password and reset the random token
857 function setPassword( $str ) {
858 $this->loadFromDatabase();
860 $this->mPassword
= $this->encryptPassword( $str );
861 $this->mNewpassword
= '';
864 # Set the random token (used for persistent authentication)
865 function setToken( $token = false ) {
866 global $wgSecretKey, $wgProxyKey, $wgDBname;
868 if ( $wgSecretKey ) {
870 } elseif ( $wgProxyKey ) {
875 $this->mToken
= md5( $key . mt_rand( 0, 0x7fffffff ) . $wgDBname . $this->mId
);
877 $this->mToken
= $token;
882 function setCookiePassword( $str ) {
883 $this->loadFromDatabase();
884 $this->mCookiePassword
= md5( $str );
887 function setNewpassword( $str ) {
888 $this->loadFromDatabase();
889 $this->mNewpassword
= $this->encryptPassword( $str );
892 function getEmail() {
893 $this->loadFromDatabase();
894 return $this->mEmail
;
897 function getEmailAuthenticationTimestamp() {
898 $this->loadFromDatabase();
899 return $this->mEmailAuthenticated
;
902 function setEmail( $str ) {
903 $this->loadFromDatabase();
904 $this->mEmail
= $str;
907 function getRealName() {
908 $this->loadFromDatabase();
909 return $this->mRealName
;
912 function setRealName( $str ) {
913 $this->loadFromDatabase();
914 $this->mRealName
= $str;
917 function getOption( $oname ) {
918 $this->loadFromDatabase();
919 if ( array_key_exists( $oname, $this->mOptions
) ) {
920 return trim( $this->mOptions
[$oname] );
926 function setOption( $oname, $val ) {
927 $this->loadFromDatabase();
928 if ( $oname == 'skin' ) {
929 # Clear cached skin, so the new one displays immediately in Special:Preferences
930 unset( $this->mSkin
);
932 $this->mOptions
[$oname] = $val;
933 $this->invalidateCache();
936 function getRights() {
937 $this->loadFromDatabase();
938 return $this->mRights
;
942 * Get the list of explicit group memberships this user has.
943 * The implicit * and user groups are not included.
944 * @return array of strings
946 function getGroups() {
947 $this->loadFromDatabase();
948 return $this->mGroups
;
952 * Get the list of implicit group memberships this user has.
953 * This includes all explicit groups, plus 'user' if logged in
954 * and '*' for all accounts.
955 * @return array of strings
957 function getEffectiveGroups() {
958 $base = array( '*' );
959 if( $this->isLoggedIn() ) {
962 return array_merge( $base, $this->getGroups() );
966 * Remove the user from the given group.
967 * This takes immediate effect.
970 function addGroup( $group ) {
971 $dbw =& wfGetDB( DB_MASTER
);
972 $dbw->insert( 'user_groups',
974 'ug_user' => $this->getID(),
975 'ug_group' => $group,
980 $this->mGroups
= array_merge( $this->mGroups
, array( $group ) );
981 $this->mRights
= User
::getGroupPermissions( $this->getEffectiveGroups() );
983 $this->invalidateCache();
984 $this->saveSettings();
988 * Remove the user from the given group.
989 * This takes immediate effect.
992 function removeGroup( $group ) {
993 $dbw =& wfGetDB( DB_MASTER
);
994 $dbw->delete( 'user_groups',
996 'ug_user' => $this->getID(),
997 'ug_group' => $group,
999 'User::removeGroup' );
1001 $this->mGroups
= array_diff( $this->mGroups
, array( $group ) );
1002 $this->mRights
= User
::getGroupPermissions( $this->getEffectiveGroups() );
1004 $this->invalidateCache();
1005 $this->saveSettings();
1010 * A more legible check for non-anonymousness.
1011 * Returns true if the user is not an anonymous visitor.
1015 function isLoggedIn() {
1016 return( $this->getID() != 0 );
1020 * A more legible check for anonymousness.
1021 * Returns true if the user is an anonymous visitor.
1026 return !$this->isLoggedIn();
1030 * Check if a user is sysop
1031 * Die with backtrace. Use User:isAllowed() instead.
1034 function isSysop() {
1035 return $this->isAllowed( 'protect' );
1039 function isDeveloper() {
1040 return $this->isAllowed( 'siteadmin' );
1044 function isBureaucrat() {
1045 return $this->isAllowed( 'makesysop' );
1049 * Whether the user is a bot
1050 * @todo need to be migrated to the new user level management sytem
1053 $this->loadFromDatabase();
1054 return in_array( 'bot', $this->mRights
);
1058 * Check if user is allowed to access a feature / make an action
1059 * @param string $action Action to be checked (see $wgAvailableRights in Defines.php for possible actions).
1060 * @return boolean True: action is allowed, False: action should not be allowed
1062 function isAllowed($action='') {
1063 $this->loadFromDatabase();
1064 return in_array( $action , $this->mRights
);
1068 * Load a skin if it doesn't exist or return it
1069 * @todo FIXME : need to check the old failback system [AV]
1071 function &getSkin() {
1073 if ( ! isset( $this->mSkin
) ) {
1074 $fname = 'User::getSkin';
1075 wfProfileIn( $fname );
1077 # get all skin names available
1078 $skinNames = Skin
::getSkinNames();
1081 $userSkin = $this->getOption( 'skin' );
1082 if ( $userSkin == '' ) { $userSkin = 'standard'; }
1084 if ( !isset( $skinNames[$userSkin] ) ) {
1085 # in case the user skin could not be found find a replacement
1089 2 => 'CologneBlue');
1090 # if phptal is enabled we should have monobook skin that
1091 # superseed the good old SkinStandard.
1092 if ( isset( $skinNames['monobook'] ) ) {
1093 $fallback[0] = 'MonoBook';
1096 if(is_numeric($userSkin) && isset( $fallback[$userSkin]) ){
1097 $sn = $fallback[$userSkin];
1102 # The user skin is available
1103 $sn = $skinNames[$userSkin];
1106 # Grab the skin class and initialise it. Each skin checks for PHPTal
1107 # and will not load if it's not enabled.
1108 require_once( $IP.'/skins/'.$sn.'.php' );
1110 # Check if we got if not failback to default skin
1111 $className = 'Skin'.$sn;
1112 if( !class_exists( $className ) ) {
1113 # DO NOT die if the class isn't found. This breaks maintenance
1114 # scripts and can cause a user account to be unrecoverable
1115 # except by SQL manipulation if a previously valid skin name
1116 # is no longer valid.
1117 $className = 'SkinStandard';
1118 require_once( $IP.'/skins/Standard.php' );
1120 $this->mSkin
=& new $className;
1121 wfProfileOut( $fname );
1123 return $this->mSkin
;
1127 * @param string $title Article title to look at
1131 * Check watched status of an article
1132 * @return bool True if article is watched
1134 function isWatched( $title ) {
1135 $wl = WatchedItem
::fromUserTitle( $this, $title );
1136 return $wl->isWatched();
1142 function addWatch( $title ) {
1143 $wl = WatchedItem
::fromUserTitle( $this, $title );
1145 $this->invalidateCache();
1149 * Stop watching an article
1151 function removeWatch( $title ) {
1152 $wl = WatchedItem
::fromUserTitle( $this, $title );
1154 $this->invalidateCache();
1158 * Clear the user's notification timestamp for the given title.
1159 * If e-notif e-mails are on, they will receive notification mails on
1160 * the next change of the page if it's watched etc.
1162 function clearNotification( &$title ) {
1163 global $wgUser, $wgUseEnotif;
1165 if ( !$wgUseEnotif ) {
1169 $userid = $this->getID();
1174 // Only update the timestamp if the page is being watched.
1175 // The query to find out if it is watched is cached both in memcached and per-invocation,
1176 // and when it does have to be executed, it can be on a slave
1177 // If this is the user's newtalk page, we always update the timestamp
1178 if ($title->getNamespace() == NS_USER_TALK
&&
1179 $title->getText() == $wgUser->getName())
1182 } elseif ( $this->getID() == $wgUser->getID() ) {
1183 $watched = $title->userIsWatching();
1188 // If the page is watched by the user (or may be watched), update the timestamp on any
1189 // any matching rows
1191 $dbw =& wfGetDB( DB_MASTER
);
1192 $success = $dbw->update( 'watchlist',
1194 'wl_notificationtimestamp' => 0
1195 ), array( /* WHERE */
1196 'wl_title' => $title->getDBkey(),
1197 'wl_namespace' => $title->getNamespace(),
1198 'wl_user' => $this->getID()
1199 ), 'User::clearLastVisited'
1207 * Resets all of the given user's page-change notification timestamps.
1208 * If e-notif e-mails are on, they will receive notification mails on
1209 * the next change of any watched page.
1211 * @param int $currentUser user ID number
1214 function clearAllNotifications( $currentUser ) {
1215 global $wgUseEnotif;
1216 if ( !$wgUseEnotif ) {
1219 if( $currentUser != 0 ) {
1221 $dbw =& wfGetDB( DB_MASTER
);
1222 $success = $dbw->update( 'watchlist',
1224 'wl_notificationtimestamp' => 0
1225 ), array( /* WHERE */
1226 'wl_user' => $currentUser
1227 ), 'UserMailer::clearAll'
1230 # we also need to clear here the "you have new message" notification for the own user_talk page
1231 # This is cleared one page view later in Article::viewUpdates();
1237 * @return string Encoding options
1239 function encodeOptions() {
1241 foreach ( $this->mOptions
as $oname => $oval ) {
1242 array_push( $a, $oname.'='.$oval );
1244 $s = implode( "\n", $a );
1251 function decodeOptions( $str ) {
1252 $a = explode( "\n", $str );
1253 foreach ( $a as $s ) {
1254 if ( preg_match( "/^(.[^=]*)=(.*)$/", $s, $m ) ) {
1255 $this->mOptions
[$m[1]] = $m[2];
1260 function setCookies() {
1261 global $wgCookieExpiration, $wgCookiePath, $wgCookieDomain, $wgDBname;
1262 if ( 0 == $this->mId
) return;
1263 $this->loadFromDatabase();
1264 $exp = time() +
$wgCookieExpiration;
1266 $_SESSION['wsUserID'] = $this->mId
;
1267 setcookie( $wgDBname.'UserID', $this->mId
, $exp, $wgCookiePath, $wgCookieDomain );
1269 $_SESSION['wsUserName'] = $this->mName
;
1270 setcookie( $wgDBname.'UserName', $this->mName
, $exp, $wgCookiePath, $wgCookieDomain );
1272 $_SESSION['wsToken'] = $this->mToken
;
1273 if ( 1 == $this->getOption( 'rememberpassword' ) ) {
1274 setcookie( $wgDBname.'Token', $this->mToken
, $exp, $wgCookiePath, $wgCookieDomain );
1276 setcookie( $wgDBname.'Token', '', time() - 3600 );
1282 * It will clean the session cookie
1285 global $wgCookiePath, $wgCookieDomain, $wgDBname, $wgIP;
1286 $this->loadDefaults();
1287 $this->setLoaded( true );
1289 $_SESSION['wsUserID'] = 0;
1291 setcookie( $wgDBname.'UserID', '', time() - 3600, $wgCookiePath, $wgCookieDomain );
1292 setcookie( $wgDBname.'Token', '', time() - 3600, $wgCookiePath, $wgCookieDomain );
1294 # Remember when user logged out, to prevent seeing cached pages
1295 setcookie( $wgDBname.'LoggedOut', wfTimestampNow(), time() +
86400, $wgCookiePath, $wgCookieDomain );
1299 * Save object settings into database
1301 function saveSettings() {
1302 global $wgMemc, $wgDBname, $wgUseEnotif;
1303 $fname = 'User::saveSettings';
1305 if ( wfReadOnly() ) { return; }
1306 $this->saveNewtalk();
1307 if ( 0 == $this->mId
) { return; }
1309 $dbw =& wfGetDB( DB_MASTER
);
1310 $dbw->update( 'user',
1312 'user_name' => $this->mName
,
1313 'user_password' => $this->mPassword
,
1314 'user_newpassword' => $this->mNewpassword
,
1315 'user_real_name' => $this->mRealName
,
1316 'user_email' => $this->mEmail
,
1317 'user_email_authenticated' => $dbw->timestampOrNull( $this->mEmailAuthenticated
),
1318 'user_options' => $this->encodeOptions(),
1319 'user_touched' => $dbw->timestamp($this->mTouched
),
1320 'user_token' => $this->mToken
1321 ), array( /* WHERE */
1322 'user_id' => $this->mId
1325 $wgMemc->delete( "$wgDBname:user:id:$this->mId" );
1329 * Save value of new talk flag.
1331 function saveNewtalk() {
1332 global $wgDBname, $wgMemc, $wgUseEnotif;
1334 $fname = 'User::saveNewtalk';
1338 if ( wfReadOnly() ) { return ; }
1339 $dbr =& wfGetDB( DB_SLAVE
);
1340 $dbw =& wfGetDB( DB_MASTER
);
1342 if ( $wgUseEnotif ) {
1343 if ( ! $this->getNewtalk() ) {
1344 # Delete the watchlist entry for user_talk page X watched by user X
1345 $dbw->delete( 'watchlist',
1346 array( 'wl_user' => $this->mId
,
1347 'wl_title' => $this->getTitleKey(),
1348 'wl_namespace' => NS_USER_TALK
),
1350 if ( $dbw->affectedRows() ) {
1354 # Anon users have a separate memcache space for newtalk
1355 # since they don't store their own info. Trim...
1356 $wgMemc->delete( "$wgDBname:newtalk:ip:{$this->mName}" );
1360 if ($this->getID() != 0) {
1362 $value = $this->getID();
1366 $value = $this->mName
;
1367 $key = "$wgDBname:newtalk:ip:$this->mName";
1370 $dbr =& wfGetDB( DB_SLAVE
);
1371 $dbw =& wfGetDB( DB_MASTER
);
1373 $res = $dbr->selectField('user_newtalk', $field,
1374 array($field => $value), $fname);
1377 if ($res !== false && $this->mNewtalk
== 0) {
1378 $dbw->delete('user_newtalk', array($field => $value), $fname);
1380 $wgMemc->set( $key, 0 );
1382 } else if ($res === false && $this->mNewtalk
== 1) {
1383 $dbw->insert('user_newtalk', array($field => $value), $fname);
1385 $wgMemc->set( $key, 1 );
1392 # Update user_touched, so that newtalk notifications in the client cache are invalidated
1393 if ( $changed && $this->getID() ) {
1394 $dbw->update('user',
1395 /*SET*/ array( 'user_touched' => $this->mTouched
),
1396 /*WHERE*/ array( 'user_id' => $this->getID() ),
1398 $wgMemc->set( "$wgDBname:user:id:{$this->mId}", $this, 86400 );
1403 * Checks if a user with the given name exists, returns the ID
1405 function idForName() {
1406 $fname = 'User::idForName';
1409 $s = trim( $this->mName
);
1410 if ( 0 == strcmp( '', $s ) ) return 0;
1412 $dbr =& wfGetDB( DB_SLAVE
);
1413 $id = $dbr->selectField( 'user', 'user_id', array( 'user_name' => $s ), $fname );
1414 if ( $id === false ) {
1421 * Add user object to the database
1423 function addToDatabase() {
1424 $fname = 'User::addToDatabase';
1425 $dbw =& wfGetDB( DB_MASTER
);
1426 $seqVal = $dbw->nextSequenceValue( 'user_user_id_seq' );
1427 $dbw->insert( 'user',
1429 'user_id' => $seqVal,
1430 'user_name' => $this->mName
,
1431 'user_password' => $this->mPassword
,
1432 'user_newpassword' => $this->mNewpassword
,
1433 'user_email' => $this->mEmail
,
1434 'user_email_authenticated' => $dbw->timestampOrNull( $this->mEmailAuthenticated
),
1435 'user_real_name' => $this->mRealName
,
1436 'user_options' => $this->encodeOptions(),
1437 'user_token' => $this->mToken
1440 $this->mId
= $dbw->insertId();
1443 function spreadBlock() {
1445 # If the (non-anonymous) user is blocked, this function will block any IP address
1446 # that they successfully log on from.
1447 $fname = 'User::spreadBlock';
1449 wfDebug( "User:spreadBlock()\n" );
1450 if ( $this->mId
== 0 ) {
1454 $userblock = Block
::newFromDB( '', $this->mId
);
1455 if ( !$userblock->isValid() ) {
1459 # Check if this IP address is already blocked
1460 $ipblock = Block
::newFromDB( $wgIP );
1461 if ( $ipblock->isValid() ) {
1462 # Just update the timestamp
1463 $ipblock->updateTimestamp();
1467 # Make a new block object with the desired properties
1468 wfDebug( "Autoblocking {$this->mName}@{$wgIP}\n" );
1469 $ipblock->mAddress
= $wgIP;
1470 $ipblock->mUser
= 0;
1471 $ipblock->mBy
= $userblock->mBy
;
1472 $ipblock->mReason
= wfMsg( 'autoblocker', $this->getName(), $userblock->mReason
);
1473 $ipblock->mTimestamp
= wfTimestampNow();
1474 $ipblock->mAuto
= 1;
1475 # If the user is already blocked with an expiry date, we don't
1476 # want to pile on top of that!
1477 if($userblock->mExpiry
) {
1478 $ipblock->mExpiry
= min ( $userblock->mExpiry
, Block
::getAutoblockExpiry( $ipblock->mTimestamp
));
1480 $ipblock->mExpiry
= Block
::getAutoblockExpiry( $ipblock->mTimestamp
);
1488 function getPageRenderingHash() {
1491 return $this->mHash
;
1494 // stubthreshold is only included below for completeness,
1495 // it will always be 0 when this function is called by parsercache.
1497 $confstr = $this->getOption( 'math' );
1498 $confstr .= '!' . $this->getOption( 'stubthreshold' );
1499 $confstr .= '!' . $this->getOption( 'date' );
1500 $confstr .= '!' . $this->getOption( 'numberheadings' );
1501 $confstr .= '!' . $this->getOption( 'language' );
1502 $confstr .= '!' . $this->getOption( 'thumbsize' );
1503 // add in language specific options, if any
1504 $extra = $wgContLang->getExtraHashOptions();
1507 $this->mHash
= $confstr;
1511 function isAllowedToCreateAccount() {
1512 return $this->isAllowed( 'createaccount' );
1516 * Set mDataLoaded, return previous value
1517 * Use this to prevent DB access in command-line scripts or similar situations
1519 function setLoaded( $loaded ) {
1520 return wfSetVar( $this->mDataLoaded
, $loaded );
1524 * Get this user's personal page title.
1529 function getUserPage() {
1530 return Title
::makeTitle( NS_USER
, $this->mName
);
1534 * Get this user's talk page title.
1539 function getTalkPage() {
1540 $title = $this->getUserPage();
1541 return $title->getTalkPage();
1547 function getMaxID() {
1548 $dbr =& wfGetDB( DB_SLAVE
);
1549 return $dbr->selectField( 'user', 'max(user_id)', false );
1553 * Determine whether the user is a newbie. Newbies are either
1554 * anonymous IPs, or the 1% most recently created accounts.
1555 * Bots and sysops are excluded.
1556 * @return bool True if it is a newbie.
1558 function isNewbie() {
1559 return $this->isAnon() ||
$this->mId
> User
::getMaxID() * 0.99 && !$this->isAllowed( 'delete' ) && !$this->isBot();
1563 * Check to see if the given clear-text password is one of the accepted passwords
1564 * @param string $password User password.
1565 * @return bool True if the given password is correct otherwise False.
1567 function checkPassword( $password ) {
1568 global $wgAuth, $wgMinimalPasswordLength;
1569 $this->loadFromDatabase();
1571 // Even though we stop people from creating passwords that
1572 // are shorter than this, doesn't mean people wont be able
1573 // to. Certain authentication plugins do NOT want to save
1574 // domain passwords in a mysql database, so we should
1575 // check this (incase $wgAuth->strict() is false).
1576 if( strlen( $password ) < $wgMinimalPasswordLength ) {
1580 if( $wgAuth->authenticate( $this->getName(), $password ) ) {
1582 } elseif( $wgAuth->strict() ) {
1583 /* Auth plugin doesn't allow local authentication */
1586 $ep = $this->encryptPassword( $password );
1587 if ( 0 == strcmp( $ep, $this->mPassword
) ) {
1589 } elseif ( ($this->mNewpassword
!= '') && (0 == strcmp( $ep, $this->mNewpassword
)) ) {
1591 } elseif ( function_exists( 'iconv' ) ) {
1592 # Some wikis were converted from ISO 8859-1 to UTF-8, the passwords can't be converted
1593 # Check for this with iconv
1594 $cp1252hash = $this->encryptPassword( iconv( 'UTF-8', 'WINDOWS-1252', $password ) );
1595 if ( 0 == strcmp( $cp1252hash, $this->mPassword
) ) {
1603 * Initialize (if necessary) and return a session token value
1604 * which can be used in edit forms to show that the user's
1605 * login credentials aren't being hijacked with a foreign form
1608 * @param mixed $salt - Optional function-specific data for hash.
1609 * Use a string or an array of strings.
1613 function editToken( $salt = '' ) {
1614 if( !isset( $_SESSION['wsEditToken'] ) ) {
1615 $token = $this->generateToken();
1616 $_SESSION['wsEditToken'] = $token;
1618 $token = $_SESSION['wsEditToken'];
1620 if( is_array( $salt ) ) {
1621 $salt = implode( '|', $salt );
1623 return md5( $token . $salt );
1627 * Generate a hex-y looking random token for various uses.
1628 * Could be made more cryptographically sure if someone cares.
1631 function generateToken( $salt = '' ) {
1632 $token = dechex( mt_rand() ) . dechex( mt_rand() );
1633 return md5( $token . $salt );
1637 * Check given value against the token value stored in the session.
1638 * A match should confirm that the form was submitted from the
1639 * user's own login session, not a form submission from a third-party
1642 * @param string $val - the input value to compare
1643 * @param string $salt - Optional function-specific data for hash
1647 function matchEditToken( $val, $salt = '' ) {
1648 return ( $val == $this->editToken( $salt ) );
1652 * Generate a new e-mail confirmation token and send a confirmation
1653 * mail to the user's given address.
1655 * @return mixed True on success, a WikiError object on failure.
1657 function sendConfirmationMail() {
1658 global $wgIP, $wgContLang;
1659 $url = $this->confirmationTokenUrl( $expiration );
1660 return $this->sendMail( wfMsg( 'confirmemail_subject' ),
1661 wfMsg( 'confirmemail_body',
1665 $wgContLang->timeanddate( $expiration, false ) ) );
1669 * Send an e-mail to this user's account. Does not check for
1670 * confirmed status or validity.
1672 * @param string $subject
1673 * @param string $body
1674 * @param strong $from Optional from address; default $wgPasswordSender will be used otherwise.
1675 * @return mixed True on success, a WikiError object on failure.
1677 function sendMail( $subject, $body, $from = null ) {
1678 if( is_null( $from ) ) {
1679 global $wgPasswordSender;
1680 $from = $wgPasswordSender;
1683 require_once( 'UserMailer.php' );
1684 $error = userMailer( $this->getEmail(), $from, $subject, $body );
1686 if( $error == '' ) {
1689 return new WikiError( $error );
1694 * Generate, store, and return a new e-mail confirmation code.
1695 * A hash (unsalted since it's used as a key) is stored.
1696 * @param &$expiration mixed output: accepts the expiration time
1700 function confirmationToken( &$expiration ) {
1701 $fname = 'User::confirmationToken';
1704 $expires = $now +
7 * 24 * 60 * 60;
1705 $expiration = wfTimestamp( TS_MW
, $expires );
1707 $token = $this->generateToken( $this->mId
. $this->mEmail
. $expires );
1708 $hash = md5( $token );
1710 $dbw =& wfGetDB( DB_MASTER
);
1711 $dbw->update( 'user',
1712 array( 'user_email_token' => $hash,
1713 'user_email_token_expires' => $dbw->timestamp( $expires ) ),
1714 array( 'user_id' => $this->mId
),
1721 * Generate and store a new e-mail confirmation token, and return
1722 * the URL the user can use to confirm.
1723 * @param &$expiration mixed output: accepts the expiration time
1727 function confirmationTokenUrl( &$expiration ) {
1728 $token = $this->confirmationToken( $expiration );
1729 $title = Title
::makeTitle( NS_SPECIAL
, 'Confirmemail/' . $token );
1730 return $title->getFullUrl();
1734 * Mark the e-mail address confirmed and save.
1736 function confirmEmail() {
1737 $this->loadFromDatabase();
1738 $this->mEmailAuthenticated
= wfTimestampNow();
1739 $this->saveSettings();
1744 * Is this user allowed to send e-mails within limits of current
1745 * site configuration?
1748 function canSendEmail() {
1749 return $this->isEmailConfirmed();
1753 * Is this user allowed to receive e-mails within limits of current
1754 * site configuration?
1757 function canReceiveEmail() {
1758 return $this->canSendEmail() && !$this->getOption( 'disablemail' );
1762 * Is this user's e-mail address valid-looking and confirmed within
1763 * limits of the current site configuration?
1765 * If $wgEmailAuthentication is on, this may require the user to have
1766 * confirmed their address by returning a code or using a password
1767 * sent to the address from the wiki.
1771 function isEmailConfirmed() {
1772 global $wgEmailAuthentication;
1773 $this->loadFromDatabase();
1774 if( $this->isAnon() )
1776 if( !$this->isValidEmailAddr( $this->mEmail
) )
1778 if( $wgEmailAuthentication && !$this->getEmailAuthenticationTimestamp() )
1784 * @param array $groups list of groups
1785 * @return array list of permission key names for given groups combined
1788 function getGroupPermissions( $groups ) {
1789 global $wgGroupPermissions;
1791 foreach( $groups as $group ) {
1792 if( isset( $wgGroupPermissions[$group] ) ) {
1793 $rights = array_merge( $rights,
1794 array_keys( array_filter( $wgGroupPermissions[$group] ) ) );
1801 * @param string $group key name
1802 * @return string localized descriptive name, if provided
1805 function getGroupName( $group ) {
1806 $key = "group-$group-name";
1807 $name = wfMsg( $key );
1808 if( $name == '' ||
$name == "<$key>" ) {
1816 * Return the set of defined explicit groups.
1817 * The * and 'user' groups are not included.
1821 function getAllGroups() {
1822 global $wgGroupPermissions;
1824 array_keys( $wgGroupPermissions ),
1825 array( '*', 'user' ) );